/* Copyright (c) 2011-2013 ISP RAS, Moscow. For more information please write to

      mermakov@ispras.ru
      svartanov@ispras.ru

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package ru.ispras.coffeemachine.instrument;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Utilities for easy handling of instrumented classfiles
 *
 * @author Mikhail Ermakov (mermakov@ispras.ru)
 * @version 1.2.0
 */

public class ClassfileSet extends HashSet<Classfile> {

	private static final long serialVersionUID = -4719629384122823897L;

	/**
	 * Returns {@link Classfile} with a specified class name
	 *
	 * @param className class name
	 * @return {@link Classfile} with a specified class name
	 */
	public Classfile find(String className) {

		if (className == null) {
			return null;
		}
		for (Classfile classfile : this) {
			if (classfile != null) {
				if (className.equals(classfile.getName())) {
					return classfile;
				}
			}
		}
		return null;
	}

	/**
	 * Returns {@link Set} of class names
	 *
	 * @return {@link Set} of class names
	 */
	public Set<String> getClasses() {

		Set<String> result = new HashSet<String>();
		for (Classfile classfile : this) {
			result.add(classfile.getName());
		}
		return result;
	}

	/**
	 * Returns <code>true</code> if the set contains a {@link Classfile} with a specified name which contains a specified method
	 *
	 * @param className fully specified class name (e.g. {@link java.lang.String java/lang/String})
	 * @param methodName fully specified method name (e.g. {@link java.lang.String#charAt(int) charAt(I)C})
	 * @return <code>true</code> if set contains specified data, <code>false</code> otherwise
	 */
	public boolean contains(String className, String methodName) {

		Classfile classfile = find(className);
		if (classfile != null) {
			if (classfile.getMethods() != null) {
				return classfile.getMethods().contains(methodName);
			}
		}
		return false;
	}

	/**
	 * Returns a {@link Set} of Jar file names
	 *
	 * @return a {@link Set} of Jar file names
	 */
	public Set<String> getJars() {

		Set<String> result = new HashSet<String>();
		for (Classfile classfile : this) {
			if (classfile.getJar() != null) {
				result.add(classfile.getJar());
			}
		}
		return result;
	}

	/**
	 * Returns a mapping of JAR file names to all packages within the corresponding JAR files
	 *
	 * @return a mapping of JAR file names to all packages within the corresponding JAR files
	 */
	public Map<String, Set<String>> getJarPackages() {

		Map<String, Set<String>> result = new HashMap<String, Set<String>>();
		for (Classfile classfile : this) {
			String jarName = classfile.getTrimmedJar();
			if (jarName == null) {
				continue;
			}
			String className = classfile.getName();
			int slashPos = className.lastIndexOf('/');
			Set<String> packageNames = result.get(jarName);
			if (packageNames == null) {
				packageNames = new HashSet<String>();
				result.put(jarName, packageNames);
			}
			String newPackageName;
			if (slashPos == -1) {
				newPackageName = "";
			} else {
				newPackageName = className.substring(0, slashPos);
			}
			boolean toAdd = true;
			for (String packageName : new HashSet<String>(packageNames)) {
				if (packageName.contains(newPackageName) && (packageName.length() > newPackageName.length())) {
					packageNames.remove(packageName);
				}
				if (newPackageName.contains(packageName) && (newPackageName.length() > packageName.length())) {
					toAdd = false;
				}
			}
			if (toAdd) {
				packageNames.add(newPackageName);
			}
		}
		return result;
	}

	/**
	 * Parses all {@link Classfiles} and adds parent method signature to child classes for instrumentation completeness
	 */
	public void addParentMethods() {

		for (Classfile classfile : this) {
			Mokka mokka = classfile.getMokka();
			Set<String> parentClasses = mokka.getParentClasses();
			for (String parentClass : parentClasses) {
				for (Classfile otherClassfile : this) {
					if (otherClassfile.getName().equals(parentClass)) {
						for (String methodName : otherClassfile.getMethods()) {
							classfile.addMethod(methodName);
						}
					}
				}
			}
		}
	}
}